package fr.grassaud.hmvc.session;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import fr.grassaud.hmvc.component.Child;
import fr.grassaud.hmvc.event.HMVCEvent;
import fr.grassaud.hmvc.info.DynamicChildInfo;
import fr.grassaud.hmvc.info.ModelInfo;
import fr.grassaud.hmvc.info.TriadInfo;
import fr.grassaud.hmvc.info.ViewInfo;
import fr.grassaud.hmvc.util.HMVCIntrospector;

public class TriadImpl implements Triad {

    private Triad parent;
    private String name;

    private TriadInfo triadInfo;

    private Object view;
    private Object model;
    private Object controller;

    private Map<String, Triad> children;
    private HMVCSessionManager session;

    private Logger logger = LoggerFactory.getLogger(TriadImpl.class);

    private AtomicBoolean releasing = new AtomicBoolean(false);

    private int dynamicChildCount = 0;

    public TriadImpl(Class<?> controllerClass, HMVCSessionManager session) {
	this.session = session;
	triadInfo = new TriadInfo(controllerClass);
	children = new HashMap<String, Triad>();
	this.name = DEFAULT_NAME;
    }

    public TriadImpl(String name, Class<?> controllerClass, HMVCSessionManager session) {
	this(controllerClass, session);
	if (name == null) {
	    throw new IllegalArgumentException("the name of a triad cannot be null");
	}
	this.name = name;
    }

    public void instantiate() {
	//
	if (controller == null) {
	    try {
		controller = triadInfo.getControllerClass().newInstance();
		if (triadInfo.getModelClass() != null) {
		    model = triadInfo.getModelClass().newInstance();
		}
		view = triadInfo.getViewClass().newInstance();
		link();
		session.fireTriadCreated(this);
		// callInitMethods();
		initChildren();
		callInitMethods();
		session.fireTriadInstantiated(this);
	    } catch (InstantiationException e) {
		logger.error("ERROR", e);
	    } catch (IllegalAccessException e) {
		logger.error("ERROR", e);
	    } catch (IllegalArgumentException e) {
		logger.error("ERROR", e);
	    } catch (InvocationTargetException e) {
		logger.error("ERROR", e);
	    } catch (NoSuchFieldException e) {
		logger.error("ERROR", e);
	    } catch (SecurityException e) {
		logger.error("ERROR", e);
	    }
	}
    }

    /**
     * Links the triad. Set model, view for each MVC component
     * 
     * @throws NoSuchFieldException
     * @throws SecurityException
     * @throws IllegalArgumentException
     * @throws IllegalAccessException
     */
    private void link() throws NoSuchFieldException, SecurityException, IllegalArgumentException,
	    IllegalAccessException {
	for (Field field : triadInfo.getModelFields()) {
	    field.setAccessible(true);
	    field.set(controller, model);

	}
	for (Field field : triadInfo.getViewFields()) {
	    field.setAccessible(true);
	    field.set(controller, view);
	}
    }

    private void initChildren() throws IllegalArgumentException, IllegalAccessException {
	for (Field child : triadInfo.getChildrenClass()) {

	    Child annotation = child.getAnnotation(Child.class);
	    String triadName = annotation.name() + "." + child.getType().getName();
	    Triad triad = children.get(triadName);
	    if (triad != null) {
		throw new IllegalStateException("The child with name " + annotation.name() + " with class "
			+ child.getName() + " was already added");
	    }
	    triad = new TriadImpl(triadName, child.getType(), session);

	    children.put(triadName, triad);
	    triad.setParent(this);
	    triad.instantiate();
	    child.setAccessible(true);
	    child.set(controller, triad.getController());

	}
    }

    private void callInitMethods() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
	Method initMethod = triadInfo.getControllerInitMethod();
	Object[] params = null;
	if (initMethod != null) {
	    initMethod.setAccessible(true);
	    initMethod.invoke(controller, params);
	}
	initMethod = triadInfo.getModelInitMethod();
	if (initMethod != null) {
	    initMethod.setAccessible(true);
	    initMethod.invoke(model, params);
	}

	initMethod = triadInfo.getViewInitMethod();
	if (initMethod != null) {
	    initMethod.setAccessible(true);
	    initMethod.invoke(view, params);
	}

    }

    public List<Triad> getTriadWithController(Class<?> controllerClass) {
	List<Triad> result = new ArrayList<Triad>();
	Set<String> names = children.keySet();
	for (String name : names) {
	    if (name.contains(controllerClass.getName())) {
		result.add(children.get(name));
	    }
	}
	if (result.isEmpty()) {
	    for (Triad t : children.values()) {
		result.addAll(t.getTriadWithController(controllerClass));
	    }
	}
	return result;
    }

    public boolean consumeEvent(HMVCEvent event) {
	Method m = null;
	switch (event.getEventType()) {
	case MODEL:
	    m = this.triadInfo.getModelEventMethod(event.getName());
	    break;
	case VIEW:
	    m = this.triadInfo.getViewEventMethod(event.getName());
	    break;
	case CONTROLLER:
	    m = this.triadInfo.getViewEventMethod(event.getName());
	    break;

	}

	if (m == null) {
	    m = this.triadInfo.getControllerEventMethod(event.getName());
	}
	return invokeMethod(m, event);

    }

    public boolean canConsumeEvent(HMVCEvent event) {
	boolean result = false;

	switch (event.getEventType()) {
	case MODEL:
	    result = this.triadInfo.getModelEventMethod(event.getName()) != null;
	    break;
	case VIEW:
	    result = this.triadInfo.getViewEventMethod(event.getName()) != null;
	    break;
	case CONTROLLER:
	    result = this.triadInfo.getControllerEventMethod(event.getName()) != null;
	    break;
	}

	if (!result) {
	    result = this.triadInfo.getControllerEventMethod(event.getName()) != null;
	}
	return result;
    }

    
    
    /**
     * Invoke the method for the given event. The event will be used to pass the parameters to the method to invoke
     * @param m the method that will be invoked
     * @param event the event that was sent for this method. 
     * @return <code>true</code> if the method was well invoked, <code>false</code> otherwise
     */
    private boolean invokeMethod(Method m, HMVCEvent event) {
	boolean result = false;
	if (m == null) {
	    throw new IllegalStateException("the method for event " + event.getName() + " cannot be found");
	}
	try {
	    m.setAccessible(true);
            //check if we need to create a child dynamically
	    if (HMVCIntrospector.isCreatedChild(m)) {
		DynamicChildInfo childInfo = new DynamicChildInfo(event, m);
		Triad child = createDynamicChildTriad(childInfo);
		Object[] params = childInfo.getParameters();
		params[childInfo.getControllerParamIndex()] = child.getController();
		m.invoke(controller, params);

	    } else {
		m.invoke(controller, event.getParameters());
	    }
	    //release the triad if the method is annotated
	    if (HMVCIntrospector.isReleaseMethod(m) && !releasing.get()) {
		release(event);
	    }

	    result = true;
	} catch (IllegalAccessException e) {
	    logger.error("ERROR", e);
	} catch (IllegalArgumentException e) {
	    logger.error("ERROR", e);
	} catch (InvocationTargetException e) {
	    logger.error("ERROR", e);
	}
	return result;

    }

    /**
     * Creates a dynamic child  for this triad
     * @param info, some dynamic info used during creation
     * @return the child triad 
     */
    private Triad createDynamicChildTriad(DynamicChildInfo info) {
	String dynaChildName = getDynamicChildName(info);
	Triad child = new TriadImpl(dynaChildName, info.getControllerClass(), session);
	children.put(dynaChildName, child);
	child.setParent(this);
	child.instantiate();
	return child;
    }

    
    /**
     * Returns the dynamic name for a triad that will be created dynamically 
     * @param info info read to create the dynamic name
     * @return the dynamic name for a triad that will be created dynamically
     */
    private String getDynamicChildName(DynamicChildInfo info) {
	String result = HMVCIntrospector.computeChildName(dynamicChildCount, info.getControllerName(),
		info.getControllerClass());
	this.dynamicChildCount++;
	return result;
    }

 
    /**
     * 
     */
    public boolean hasComponent(Object o) {
	return o.equals(controller) || o.equals(view) || o.equals(model);
    }

    public Triad getParent() {
	return parent;
    }

    public void setParent(Triad parent) {
	this.parent = parent;
    }

    @SuppressWarnings("unchecked")
    public <T> T getView() {
	return (T) view;
    }

    @SuppressWarnings("unchecked")
    public <T> T getModel() {
	return (T) model;
    }

    @SuppressWarnings("unchecked")
    public <T> T getController() {
	return (T) controller;

    }

    public Map<String, Triad> getChildren() {
	return this.children;
    }

    public TriadInfo getTriadInfo() {
	return this.triadInfo;
    }

    public void release(HMVCEvent event) {
	releasing.set(true);
	// release this triad and all chidlren
	Method m = this.triadInfo.getReleaseMethod();
	releaseView(event);
	releaseModel(event);
	boolean result = this.invokeMethod(m, event);
	Class<?> parentClass = parent.getTriadInfo().getControllerClass();
	Triad triadParent = getParent();
	if (triadParent != null) {
	    triadParent.removeChild(this);
	}
	Set<String> copyOfNames = new HashSet<String>(children.keySet());
	for (String childName : copyOfNames) {
	    Triad t = children.get(childName);
	    t.release(event);
	}

	if (result) {
	    logger.info("The controller {}  was release from it's parent {}", this.triadInfo.getControllerClass(),
		    parentClass);
	} else {
	    logger.error("The release method was not consumed for controller {}", this.triadInfo.getControllerClass());

	}
	releasing.set(false);
    }

    
    /**
     * Calls the release method for the view
     * @param event the event used to release this triad
     */
    private void releaseView(HMVCEvent event) {
	for (ViewInfo info : triadInfo.getAllViewInfo()) {
	    Method releaseMethod = info.getReleaseMethod();

	    if (releaseMethod != null) {

		if (invokeMethod(releaseMethod, event)) {
		    logger.info("The view {}  was release from it's controller {}", info.getViewClass(),
			    info.getControllerClass());
		} else {
		    logger.error("The release method was not consumed for view {}", info.getViewClass());

		}
	    }
	}
    }

    /**
     * Calls the release method for the model
     * @param event the event used to release this triad
     */
    private void releaseModel(HMVCEvent event) {
	for (ModelInfo info : triadInfo.getAllModelInfo()) {
	    Method releaseMethod = info.getReleaseMethod();

	    if (releaseMethod != null) {

		if (invokeMethod(releaseMethod, event)) {
		    logger.info("The model {}  was release from it's controller {}", info.getModelClass(),
			    info.getControllerClass());
		} else {
		    logger.error("The release method was not consumed for model {}", info.getModelClass());

		}
	    }
	}
    }

    /**
     * Sets the name of this triad
     * @param name
     */
    public void setName(String name) {
	this.name = name;
    }

    public String getName() {
	return this.name;
    }

    @Override
    public void removeChild(Triad t) {
	if (children.remove(t.getName()) != null) {
	    t.setParent(null);
	}
    }
}
